查看原文
其他

凡猿修仙传:斩杀HardwareRenderer.nSetStopped ANR

三雒 鸿洋
2024-08-24

本文作者


作者:三雒

链接:

https://juejin.cn/post/7351687181381517322

本文由作者授权发布。


1nSetStopped大妖


HardwareRenderer.nSetStopped 是我们App ANR中战力第四的大妖,每天殃及用户8000余人,我们一直都想除之而后快。
ANR: COMBINED_FAIL: Application did not respond to UI input
at syscall (/apex/com.android.runtime/lib64/bionic/libc.so:28)
at __futex_wait_ex (/apex/com.android.runtime/lib64/bionic/libc.so:144)
at pthread_cond_wait (/apex/com.android.runtime/lib64/bionic/libc.so:72)
at std::__1::condition_variable::wait (/system/lib64/libc++.so:20)
at std::__1::__assoc_sub_state::copy (/system/lib64/libc++.so:84)
at std::__1::future<void>::get (/system/lib64/libc++.so:24)
at android::uirenderer::renderthread::RenderProxy::setStopped (/system/lib64/libhwui.so:364)
at android.graphics.HardwareRenderer.nSetStopped (HardwareRenderer.java:-2)
at android.graphics.HardwareRenderer.setStopped (HardwareRenderer.java:498)
at android.view.ViewRootImpl.performDraw (ViewRootImpl.java:4428)
at android.view.ViewRootImpl.performTraversals (ViewRootImpl.java:3610)
at android.view.ViewRootImpl.doTraversal (ViewRootImpl.java:2379)
at android.view.ViewRootImpl$TraversalRunnable.run (ViewRootImpl.java:9138)
at android.view.Choreographer$CallbackRecord.run (Choreographer.java:1234)
at android.view.Choreographer$CallbackRecord.run (Choreographer.java:1242)
at android.view.Choreographer.doCallbacks (Choreographer.java:902)
at android.view.Choreographer.doFrame (Choreographer.java:835)
at android.view.Choreographer$FrameDisplayEventReceiver.run (Choreographer.java:1217)
at android.os.Handler.handleCallback (Handler.java:942)
at android.os.Handler.dispatchMessage (Handler.java:99)
at android.os.Looper.loopOnce (Looper.java:201)
at android.os.Looper.loop (Looper.java:288)
at android.app.ActivityThread.main (ActivityThread.java:8061)
at java.lang.reflect.Method.invoke (Method.java:-2)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:703)
at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:923)

奈何此妖修为极深,从堆栈上只能看出其是在主线程渲染帧过程中,在View的三大流程的最后一步performDraw时候作案。从表面找出其破绽并不容易,但ANR率长期居高不下,用户苦其久矣,我们痛定思痛,定要斩杀这大妖。

2寻找大妖破绽

在修仙界我们分析破绽通常是由表及里,由浅到深,所以我们先从案发现场看看有没有什么有用的信息。

案发现场

线索一

得益于我们的法器APMS的特征聚类能力,很容易能发现作案时间主要集中在应用启动的30s内。

线索二

由于ANR只是一个结果,而这个结果可能是一段时间内不同大妖连续作案共同导致的,而瞬时采集的堆栈会出现抓偏,错怪好人当大妖的情况,所以我们需要做一次二次确认。这里就得再借助一下我们APMS的快速抓栈能力,把案发时候主线程的Trace信息上报上来。于是通过上图可以确认nSetStopped大妖确实耗费了600多ms,对最终ANR的贡献显著。
现场也就这些线索,那接下来我们不得深入分析下大妖本身是怎么作案的,也就是怎么造成这么多耗时,接下来就是剖析nSetStopped方法。

剖析nSetStopped

从ANR堆栈中可以看nSetStopped 是从HardwareRenderer.setStopped方法调用而来的,先来看一下这个方法的作用。根据官方注释这个方法是用来控制内容是否渲染到Surface中去,当stopped为true时候会停止渲染,当stopped为false时候会恢复渲染。一个典型的使用场景是当Activiry.start时候需要恢渲染到Surface,当Activity.stop之后需要停止渲染。
/**
     * Hard stops rendering into the surface. If the renderer is stopped it will
     * block any attempt to render. Calls to {@link FrameRenderRequest#syncAndDraw()} will
     * still sync over the latest rendering content, however they will not render and instead
     * {@link #SYNC_CONTEXT_IS_STOPPED} will be returned.
     *
     * <p>If false is passed then rendering will resume as normal. Any pending rendering requests
     * will produce a new frame at the next vsync signal.
     *
     * <p>This is useful in combination with lifecycle events such as {@link Activity#onStop()}
     * and {@link Activity#onStart()}.
     *
     * @param stopped true to stop all rendering, false to resume
     * @hide
     */
    public void setStopped(boolean stopped) {
        nSetStopped(mNativeProxy, stopped);
    }
进一步看nSetStopped方法,它一个Native方法,对应的实现如下:这里把Java层传入的的renderProxy地址强转为RenderProxy指针,并调用了RenderProxy的setStopped方法。RenderProxy其实就是对RenderThread的一层代理,大部分方法都是往RenderThread的的工作队列添加任务,这里的setStopped其实也是一样的。
static void android_view_ThreadedRenderer_setStopped(JNIEnv* env, jobject clazz,
        jlong proxyPtr, jboolean stopped) 
{
    RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr);
    proxy->setStopped(stopped);
}
可以看出这里调用了mRenderThread.queue().runSync 方法,并塞进了一个lambda表达式进去,其执行了 CanvasContext的setStopped方法。
void RenderProxy::setStopped(bool stopped) {
    mRenderThread.queue().runSync([this, stopped]() { mContext->setStopped(stopped); });
}
CanvasContext的setStopped方法执行了一长串内容,但都不重要,重要的是第一行就判断stopped的值是否变化了,也就是说如果参数stopped的值是flase的话这个方法就相当于执不执行都没有任何意义,也不会怎么耗时。
//初始值
bool mStopped = false;

void CanvasContext::setStopped(bool stopped) {
    //判断值是否改变
    if (mStopped != stopped) {
        mStopped = stopped;
        if (mStopped) {
            mGenerationID++;
            mRenderThread.removeFrameCallback(this);
            mRenderPipeline->onStop();
            mRenderThread.cacheManager().onContextStopped(this);
        } else if (mIsDirty && hasOutputTarget()) {
            mRenderThread.postFrameCallback(this);
        }
    }
}
到这里我们通过前面的分析知道stopped为false其实表示可以渲染到Surface, 并且在Activity start时候会调用一次,这次调用参数也传递的是false, 那么这种调用其实就是无效调用。
再结合线索一 可以发现大妖作案的时机集中在启动阶段,这些受害用户从操作轨迹上看也没有切换Activity , 那我们可以大胆猜测这些用户被害时候其实也是无效调用,那么为什么会耗时呢?
答案就还是隐藏mRenderThread.queue().runSync 方法中,RenderThread是类似主线程消息队列一样的模型,runSync方法需要等待返任务执行完成,那么如果runSync时RenderThread任务比较重的话就可能其他任务阻塞等待。
template <class F>
 auto runSync(F&& func) -> decltype(func()) {
        std::packaged_task<decltype(func())()> task{std::forward<F>(func)};
        post([&task]()
 { std::invoke(task); });
        //等待结果
        return task.get_future().get();
    };
到这里作案的时机、动机甚至过程细节我们都了解了,用一句话描述就是:
在启动阶段主线程调用HardwareRenderer.setStopped通知RenderThread线程可以向Suface渲染,由于RenderThread线程任务过重而产生等待,而默认就是可以渲染到Suface的,所以这种调用是无效的。

既然是无效的那我们的破敌之法也就比较清晰了。

3破敌之法


这个方法是无用调用,我们直接在上层不调用不就行了,就像下面这样就可避免耗时。但是修仙系统运转有自己的法则,我们想要偷梁换柱必须突破禁制。
HardwareRenderer:
boolean mStopped = false;
public void setStopped(boolean stopped) {
   if(mStopped != stopped){
      mStopped = stopped;
      nSetStopped(mNativeProxy, stopped);
   }  
}

剑一 :反射+动态代理

HardwareRenderer作为Framework的一部分,我们只能在运行时hook,在Java层通常的思路就是反射+动态代理把原始的对象包一层替换掉,但可惜setStopped并不是接口方法。

剑二:ArtMethod替换字节码

就像AndFix一样在运行时直接把setStopped的方法的字节码完全替换掉,理论上或许可行,但是目前也没有成熟的方案。

剑三:PLT hook

既然Java层hook不行,那我们就更进一步从Native层尝试hook。我们首先使用的是相对稳定的PLT hook, PLT hook的是调用方的so的GOT表查询,我们这个场景是从虚拟机libart.so调用到 libwhui.soandroid_view_ThreadedRenderer_setStopped方法,但是不幸的是这个JNI方法是动态注册,这也就意味着libwhui.so不会导出该函数符号,libart.so调用时也不会有GOT表查询过程,也就没法hook。
static const JNINativeMethod gMethods[] = {
        {"nSetStopped""(JZ)V", (void*)android_view_ThreadedRenderer_setStopped}
 }

剑四: JNI hook

该思路如其名就是直接替换JNI方法,我们可以将android_view_ThreadedRenderer_setStopped替换到我们自定义的一个函数地址上。基本原理是对于Java native方法而言其ArtMethod的data_字段中保存的就是该方法对应的JNI函数地址,我们只需要替换这个地址就可以实现JNI函数的逻辑替换,具体实现可以看这篇 JNI函数 Hook实战 ,但该方案似乎没有经过大量的应用验证,考虑到稳定性暂时也没有使用该方法。

https://juejin.cn/post/7268894037464367140


剑五:Inline hook

最后终于祭出大招Inline hook, 我们知道Inline hook可以替换方法的内容,但是担心其稳定性,好在修仙界的顶流帮忙ByteDance开源了其android-inline-hook , 据说稳定性极好。

https://github.com/bytedance/android-inline-hook


Inline hook是可以通过 “函数地址” hook 无符号信息的函数的,也就应该可以hook JNI函数android_view_ThreadedRenderer_setStopped, 但对方法长度有一些要求,并且我们需要自己获取JNI原函数的地址相对麻烦。
所以我们不妨在进一层直接hook RenderProxy的setStopped方法,这个方法恰好是在libhwui.so中导出的, 直接通过函数符号hook。
void RenderProxy::setStopped(bool stopped) {
    mRenderThread.queue().runSync([this, stopped]() { mContext->setStopped(stopped); });
}
hook后的代理函数如下:
void proxySetStopped(void *renderProxy, bool stop) {
    SHADOWHOOK_STACK_SCOPE();
    bool lastStop = findStopStatus(renderProxy);
    if (stop != lastStop) {
        SHADOWHOOK_CALL_PREV(proxySetStopped, renderProxy, stop);
        setStopStatus(renderProxy, stop);
    }
}

bool findStopStatus(void *thiz) {
    std::lock_guard<std::mutex> lock(proxyMutex);
    bool lastStop = false;
    auto it = proxy_map.find(thiz);
    if (it != proxy_map.end()) {
        lastStop = it->second;
    }
    return lastStop;
}

void setStopStatus(void *thiz, bool stop) {
    std::lock_guard<std::mutex> lock(proxyMutex);
    proxy_map[thiz] = stop;
}

这里使用Map保存状态的的原因是RenderProxy是每个Window对应一个,需要分别保存,我方九剑只出五剑敌方已经元气大伤。

4除妖成果

修复后该问题的下降趋势如下图,大概消灭掉60%多的大妖势力,剩下的问题可能还要从业务本身触发,对视图层级、过度绘制、异常渲染指令等进行优化,从而尽量减轻RenderThread的负载。

对于ANR率的优化绝对值优化0.05%


5境界精进


  • 对于问题的探究我们始终遵从第一性原理,对于事物本身了解的越详细越透彻越我们能找到越多越合适的方案。
  • Android上的hook手段非常多,从时机上可以分为编译时hook和运行时hook两大类。编译时hook主要以构建过程中的字节码修改为主,代表有Lancet、ByteX等;运行时hook可以细分为Java层和Native层hook,Java层常用的有反射替换、动态代理、ArtTI等 , Native层的Hook主要就是以PLT 和 inline hook为主,还有一些特定场景使用的比如JNI hook,虚函数hook、根据内存偏移选址变量并修改等。
  • 关于hook技术我们要抱有积极正确的态度,它是一把双刃剑,不可滥用也不能固步自封,使用它解决问题的过程中要谨慎衡量对稳定性、业务指标等影响。


最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!


推荐阅读

用BuildSrc管理Android依赖版本已经过时了?Catalogs才是版本答案?
迈向 Android 架构师:模块化设计原则
编译优化之Gradle最佳配置实践


扫一扫 关注我的公众号

如果你想要跟大家分享你的文章,欢迎投稿~


┏(^0^)┛明天见!

继续滑动看下一个
鸿洋
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存